package jp.co.nes.awssupport.compile.batch;

import java.io.IOException;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.services.s3.model.S3ObjectSummary;

import jp.co.nes.awssupport.business.bean.BillingCsvBean;
import jp.co.nes.awssupport.business.bean.CommonContainerBean;
import jp.co.nes.awssupport.business.bean.CommonContainerBean.ProcessingResult;
import jp.co.nes.awssupport.business.bean.CommonInfoBean;
import jp.co.nes.awssupport.business.bean.ManifestBean;
import jp.co.nes.awssupport.business.constant.LineItemType;
import jp.co.nes.awssupport.business.logic.BillingCsvReader;
import jp.co.nes.awssupport.business.logic.CompileProgressManage;
import jp.co.nes.awssupport.business.logic.ReservedManageProcessing;
import jp.co.nes.awssupport.business.utility.BillingCsvUtil;
import jp.co.nes.awssupport.business.utility.ManifestHelper;
import jp.co.nes.awssupport.business.utility.ReservedHelper;
import jp.co.nes.awssupport.common.base.AbstractActivityTaskClient;
import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.utility.AppInfoUtil;
import jp.co.nes.awssupport.common.utility.BeanUtil;
import jp.co.nes.awssupport.common.utility.DateUtil;
import jp.co.nes.awssupport.common.utility.DbConnUtil;

/**
 * リザーブド管理出力です。
 */
public class ReservedManage extends AbstractActivityTaskClient<CommonInfoBean, CommonContainerBean<CommonInfoBean>> {

    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(ReservedManage.class);

    /** アクティビティのarn(コマンドライン引数で設定) */
    @Option(name = "-aarn", aliases = "--activityArn", usage = "アクティビティのarn", metaVar = "activityArn", required = true)
    private String activityArn;

    /** 請求CSVのマニュフェストを保持するマップ */
    private Map<String, ManifestBean> manifestMap;

    /**
     * メインメソッドです。
     *
     * @param args コマンドライン引数
     */
    public static void main(String[] args) {
        ReservedManage client = new ReservedManage();
        client.execute(args);
    }

    /**
     * デフォルトコンストラクタです。
     */
    public ReservedManage() {
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractStepFunctionsCallBatchClient#getActivityArn()
     */
    @Override
    protected String getActivityArn() {
        return activityArn;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractStepFunctionsCallBatchClient#getInputClass()
     */
    @Override
    protected Class<CommonInfoBean> getInputClass() {
        return CommonInfoBean.class;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractActivityTaskClient#runActivityTask(jp.co.nes.awssupport.common.bean.BaseBean)
     */
    @Override
    protected CommonContainerBean<CommonInfoBean> runActivityTask(CommonInfoBean input) {
        String categoryCsv = "BillingCsv";
        String bucketName = AppInfoUtil.getValAndEmptyChk(categoryCsv, "BucketName");
        String objectKey = AppInfoUtil.getValAndEmptyChk(categoryCsv, "ObjectKey");
        String manifestObjectKey = AppInfoUtil.getValAndEmptyChk(categoryCsv, "ManifestObjectKey");

        String category = "ReservedManage";
        int max = AppInfoUtil.getIntValAndMinChk(category, "BatchExecuteMaxCount", 5000, 1);

        if (logger.isDebugEnabled()) {
            logger.debug(categoryCsv + ",BucketName=" + bucketName);
            logger.debug(categoryCsv + ",ObjectKey(before formatting)=" + objectKey);
            logger.debug(categoryCsv + ",ManifestObjectKey(before formatting)=" + manifestObjectKey);
            logger.debug(category + "BatchExecuteMaxCount=" + max);
        }

        String targetYearMonthValue = input.getTargetYearMonthValue();
        YearMonth targetYearMonth = DateUtil.toYearMonth(targetYearMonthValue);
        CompileProgressManage cpManage = new CompileProgressManage(targetYearMonth);
        List<String> linkedAccountIdList = cpManage.getLinkedAccountIdList();
        List<S3ObjectSummary> splitBillingCsvList = getSplitBillingCsvList(bucketName, targetYearMonth);

        // 出力する請求CSVの情報をリストにparallelを使用して、リストに登録する。
        // -> parallelでRDSへの出力まで実施すると、コネクションをスレッドで管理する方法が必要になり、
        //    また、コネクション数の管理も必要になるので、ここでは、読み込みに時間がかかると想定される、
        //    s3からの読み込みをparallelで実施し、RDSに出力する内容をリストに登録する。
        //    その後、リストに登録されている内容をRDSに出力する。
        //    なお、リストに登録される数は、極端に大きなものにならない想定であり、メモリ不足にはならない想定。
        Map<String, BillingCsvBean> insertMap = new HashMap<>();
        linkedAccountIdList.stream().parallel().forEach(linkedAccountId -> {
            List<String> payerAccountIdList = BillingCsvReader.getPayerAccountId(splitBillingCsvList, linkedAccountId);
            if (payerAccountIdList.isEmpty()) {
                // 通常発生しない。
                throw new CommonException("payerAccountId is empty");
            }

            for (String payerAccountId : payerAccountIdList) {
                String oKey = String.format(objectKey, targetYearMonthValue, payerAccountId, linkedAccountId);
                String moKey = String.format(manifestObjectKey, targetYearMonthValue, payerAccountId);

                ManifestBean manifestBean = getManifestBean(bucketName, moKey);
                try (BillingCsvReader reader = createBillingCsvReader(manifestBean, bucketName, oKey)) {
                    while (reader.readBean()) {
                        if (reader.isHeader()) {
                            // ファイルの1行目の場合
                            continue;
                        }

                        if (reader.isInvalidLine()) {
                            // 分割後のファイルを読み込んでいるので、通常は発生しない。
                            throw new CommonException("Read invalid line");
                        }

                        BillingCsvBean bean = reader.getBean();
                        if (!BillingCsvUtil.isReservedUpfrontOrMonthlyFixed(bean)) {
                            // リザーブインスタンスの「All Upfront」or「Partial Upfront」に関する前払い料金 and
                            // リザーブインスタンスの「Partial Upfront」or「No Upfront」に関する定額月額 RI 料金 でない。
                            continue;
                        }

                        // insertMapは、parallelの中でsynchronizedする必要がある。
                        synchronized (insertMap) {
                            String key = bean.getReservationARN();
                            if (insertMap.containsKey(key)) {
                                // ReservationARNに同じものが存在した場合
                                // なお、前払い料金と定額月額 RI 料金で、2つ存在した場合についてを想定して処理する。

                                BillingCsvBean containBean = insertMap.get(key);

                                // insertMapに含まれている請求CSVのLineItemTypeと読み込んだ請求CSVのLineItemTypeをチェックする。
                                boolean isProceed = checkLineItemType(containBean, bean);
                                if (!isProceed) {
                                    continue;
                                }

                                LineItemType containLineItemType = containBean.getLineItemType();
                                if (LineItemType.RI_FEE == containLineItemType) {
                                    // Mapに含まれていた内容が定額月額 RIだった場合は、前払い料金を優先させ、Mapの内容を置換する。
                                    insertMap.put(key, bean);
                                }
                            } else {
                                insertMap.put(key, bean);
                            }
                        }
                    }
                } catch (IOException e) {
                    throw new CommonException(e);
                }
            }
        });

        try {
            // insertMapに登録されている内容をRDSに出力する。
            ReservedManageProcessing rmProcessing = createReservedManageProcessing(targetYearMonth);
            rmProcessing.save(new ArrayList<BillingCsvBean>() {
                {
                    addAll(insertMap.values());
                }
            }, max);

            // コミット処理が正常に終了した後、何らかの異常が発生し、アプリケーションが異常で終了した場合は、
            // 異常で終了したが、データ(リザーブド管理の内容)には不整合が発生していない状況である。
            // この状況は、ほとんど発生しないと考えている。
            // リカバリついては、単純に再実行すれば問題ない状況である。
            DbConnUtil.commitAll();
        } catch (Throwable e) {
            DbConnUtil.rollbackAll();
            throw new CommonException(e);
        } finally {
            DbConnUtil.closeAll();
        }

        // 返却内容の作成
        CommonInfoBean info = new CommonInfoBean();
        BeanUtil.copyProperties(info, input);
        CommonContainerBean<CommonInfoBean> output = new CommonContainerBean<>();
        output.setInfoBean(info);
        output.setProcessingResult(ProcessingResult.SUCCESS);
        return output;
    }

    /**
     * 請求CSVの一覧を返却します。
     *
     * @param bucketName バケット名
     * @param targetYearMonth 処理対象年月
     * @return 請求CSVの一覧
     */
    protected List<S3ObjectSummary> getSplitBillingCsvList(String splitBucketName, YearMonth targetYearMonth) {
        return BillingCsvReader.getSplitBillingCsvList(splitBucketName, targetYearMonth);
    }

    /**
     * 請求CSVのマニュフェストを返却します。
     *
     * @param bucketName バケット名
     * @param objectKey オブジェクトキー
     * @return 請求CSVのマニュフェスト
     */
    // 本メソッドは、streamのparallel処理の中で呼ばれているため、メソッドの中で使用するmanifestMapをsynchronizedする必要がある。
    // よって、synchronizedメソッドとして実装した。
    protected synchronized ManifestBean getManifestBean(String bucketName, String objectKey) {
        if (manifestMap == null) {
            manifestMap = new HashMap<>();
        }

        String key = bucketName + objectKey;
        ManifestBean manifestBean = manifestMap.get(key);
        if (manifestBean != null) {
            return manifestBean;
        }

        ManifestHelper manifestHelper = new ManifestHelper(bucketName, objectKey);
        manifestBean = manifestHelper.getManifestBean();
        manifestMap.put(key, manifestBean);
        return manifestBean;
    }

    /**
     * 請求CSVReaderを返却します。
     *
     * @param manifestBean マニュフェスト
     * @param bucketName バケット名
     * @param objectKey オブジェクトキー
     * @return 請求CSVReader
     */
    protected BillingCsvReader createBillingCsvReader(ManifestBean manifestBean, String bucketName, String objectKey) {
        return new BillingCsvReader(manifestBean, bucketName, objectKey);
    }

    /**
     * マップに含まれている請求CSVのLineItemTypeと読み込んだ請求CSVのLineItemTypeをチェックします。<br>
     * チェック結果で異常があった場合は、例外をスローします。
     *
     * @param containBean マップに含まれている請求CSV
     * @param bean 読み込んだ請求CSV
     * @return true: そのまま処理を進めます、false: 次の行を読み込む処理に進めます。
     */
    private boolean checkLineItemType(BillingCsvBean containBean, BillingCsvBean bean) {
        LineItemType containLineItemType = containBean.getLineItemType();
        LineItemType beanLineItemType = bean.getLineItemType();
        String containLinkedAccountId = containBean.getLinkedAccountId();
        String beanLinkedAccountId = bean.getLinkedAccountId();

        if (LineItemType.FEE != containLineItemType && LineItemType.RI_FEE != containLineItemType) {
            // 本メソッドを呼び出す前に、!BillingCsvUtil.isReservedUpfrontOrMonthlyFixed(bean))の処理で
            // FeeやRIFee以外は、除外されているはずなので通常発生しない。
            StringBuilder builder = new StringBuilder();
            builder.append("LineItemType is wrong(containLineItemType)(LinkedAccountId=");
            builder.append(containLinkedAccountId);
            builder.append(")");
            throw new CommonException(builder.toString());
        }

        if (LineItemType.FEE != beanLineItemType && LineItemType.RI_FEE != beanLineItemType) {
            // 本メソッドを呼び出す前に、!BillingCsvUtil.isReservedUpfrontOrMonthlyFixed(bean))の処理で
            // FeeやRIFee以外は、除外されているはずなので通常発生しない。
            StringBuilder builder = new StringBuilder();
            builder.append("LineItemType is wrong(beanLineItemType)(LinkedAccountId=");
            builder.append(beanLinkedAccountId);
            builder.append(")");
            throw new CommonException(builder.toString());
        }

        if (LineItemType.FEE == containLineItemType && LineItemType.FEE == beanLineItemType) {
            // 本メソッドは、ReservationARNが同じ場合に呼び出されるので、
            // この場合、Feeが、2つ存在しないと想定しているのでエラーとする。
            StringBuilder builder = new StringBuilder();
            builder.append("LineItemType is wrong(Fee, Fee)(LinkedAccountId=");
            builder.append(containLinkedAccountId);
            builder.append(",");
            builder.append(beanLinkedAccountId);
            builder.append(")");
            throw new CommonException(builder.toString());
        }

        if (LineItemType.RI_FEE == containLineItemType && LineItemType.RI_FEE == beanLineItemType) {
            String containPayerAccountId = containBean.getPayerAccountId();
            String beanPayerAccountId = bean.getPayerAccountId();
            if (containLinkedAccountId != null && containLinkedAccountId.equals(beanLinkedAccountId) &&
                    containPayerAccountId != null && !containPayerAccountId.equals(beanPayerAccountId)) {
                // 本メソッドは、ReservationARNが同じ場合に呼び出されるので、
                // この場合、連結アカウントIDが同じ and 一括請求アカウントIDが異なれば、
                // 一括請求アカウントIDが変更され、変更元と変更先で、使用量が分割された状況で、
                // 同じReservationARNのRI_FEEが2つ出力されていると想定し、
                // 次の行を読み込む処理に進めるためfalseを返却する。
                return false;
            }

            StringBuilder builder = new StringBuilder();
            builder.append("LineItemType is wrong(RIFee, RIFee)(LinkedAccountId=");
            builder.append(containLinkedAccountId);
            builder.append(",");
            builder.append(beanLinkedAccountId);
            builder.append(")");
            throw new CommonException(builder.toString());
        }

        return true;
    }

    /**
     * リザーブド管理処理を生成します。
     *
     * @param targetYearMonth 処理対象年月
     * @return リザーブド管理処理
     */
    protected ReservedManageProcessing createReservedManageProcessing(YearMonth targetYearMonth) {
        ReservedHelper helper = createReservedHelper();
        return new ReservedManageProcessing(targetYearMonth, helper);
    }

    /**
     * リザーブドヘルパーを生成します。
     *
     * @return リザーブドヘルパー
     */
    protected ReservedHelper createReservedHelper() {
        return new ReservedHelper();
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractClient#debugArgument()
     */
    @Override
    protected void debugArgument() {
        if (logger.isDebugEnabled()) {
            logger.debug("activityArn=" + activityArn);
        }
    }
}
